import sys
import operator

from inspect import getmro, getclasstree, getdoc, getcomments

from constants import NewId
from constants import object_types, _panelColumns, _moduleToIcon
from constants import NO_DIFF, KIND_DIFF, SIGNATURE_DIFF, CLASSTREE_DIFF
from constants import GETSET_DIFF, ONE_ONLY, TWO_ONLY, LOC_DIFF, FIRSTLINE_DIFF
from constants import VALUE_DIFF, DOCS_DIFF

reload(sys)
sys.setdefaultencoding("utf-8")


def make_class_tree(tree):

    class_tree = []
    
    if isinstance(tree, list):
        for node in tree:
            class_tree.append(make_class_tree(node))
    else:
        name = tree[0].__name__
        class_tree.append(name)
        
    return class_tree


def print_class_tree(klass):

    text = "\n\n\n\nSub & super classes\n\n\n"
    
    for indx, item in enumerate([klass.subClasses, klass.superClasses]):

        text += "Known "
        current = (indx == 0 and ["subclasses"] or ["superclasses"])[0]
        text += current + "\n\n"

        if item:
            for v in item:
                text += u"- %s\n"%v
        else:
            text += 'No %s found\n'%current

        text += "\n\n"

    text += "Class tree\n\n"
    text += build_sub_tree(klass.class_tree)
    return text
    

def build_sub_tree(class_tree, text='', indent=-1):

    if isinstance(class_tree, list):
        for node in class_tree:
            text += build_sub_tree(node, '', indent+1)
    else:
        text = '  ' * (indent-1) + '- ' + class_tree + "\n"

    return text


def to_html(klass):

    text = "General description\n\n"
    values = klass.FormatOutput()
    name = klass.obj_type.split("/")[0]
    columns = _panelColumns[name]

    columnNames = [item[0] for item in columns]
    formats = []
    
    for columnName, value in zip(columnNames, values):
        if 'Number of' in columnName:
            columnName = 'LOC'
        maxLen = max(len(columnName), len("%s"%value))
        format = "%%-%ds"%(maxLen + 4)
        text += format%columnName
        formats.append(format)

    text += "\n"            
    for indx, value in enumerate(values):
        text += formats[indx]%value

    if name == "Class":
        text += print_class_tree(klass)

    text += "\n\n\nDocstrings\n"
    comments = klass.comments
    
    if comments:
        text += '\n%s\n'%comments

    if name in ["Class", "Method"]:
        if not comments:
            text += "\n"
        text += klass.signature + "\n"
        
    text += klass.docs + "\n"

    return text


def GetTopLevelParent(klass):

    parent = klass.parent

    if not parent:
        return klass
    
    parents = [parent]
    
    while parent:
        parent = parent.parent
        parents.append(parent)

    return parents[-2]


class InheritanceDiagram(object):
    """
    Given a list of classes, determines the set of classes that they inherit
    from all the way to the root "object", and then is able to generate a
    graphviz dot graph from them.
    """

    def __init__(self, classes):
        
        self.class_info, self.specials = self._class_info(classes)

        
    def _class_info(self, classes):
        """Return name and bases for all classes that are ancestors of
        *classes*.

        *parts* gives the number of dotted name parts that is removed from the
        displayed node names.
        """
        
        all_classes = {}
        specials = []

        def recurse(cls):
            nodename = fullname = self.class_name(cls)

            baselist = []
            all_classes[cls] = (nodename, fullname, baselist)

            for base in cls.__bases__:
                baselist.append(self.class_name(base))
                if base not in all_classes:
                    recurse(base)

        for cls in classes:
            recurse(cls)
            specials.append(self.class_name(cls))

        return all_classes.values(), specials


    def class_name(self, cls):
        """Given a class object, return a fully-qualified name.

        This works for things I've tested in matplotlib so far, but may not be
        completely general.
        """
        
        module = cls.__module__

        if module == '__builtin__':
            fullname = cls.__name__
        else:
            fullname = '%s.%s' % (module, cls.__name__)

        return fullname


    # These are the default attrs for graphviz
    default_graph_attrs = {
        'rankdir': 'LR',
        'size': '"8.0, 12.0"',
    }
    default_node_attrs = {
        'shape': 'box',
        'fontsize': 9,
        'height': 0.3,
        'fontname': 'Vera Sans, DejaVu Sans, Liberation Sans, '
                    'Arial, Helvetica, sans',
        'style': '"setlinewidth(0.5)"',
    }
    default_edge_attrs = {
        'arrowsize': 0.5,
        'style': '"setlinewidth(0.5)"',
    }

    def _format_node_attrs(self, attrs):
        return ','.join(['%s=%s' % x for x in attrs.items()])

    def _format_graph_attrs(self, attrs):
        return ''.join(['%s=%s;\n' % x for x in attrs.items()])

    def generate_dot(self, name="dummy", urls={}, graph_attrs={}, node_attrs={}, edge_attrs={}):
        """Generate a graphviz dot graph from the classes that were passed in
        to __init__.

        *name* is the name of the graph.

        *urls* is a dictionary mapping class names to HTTP URLs.

        *graph_attrs*, *node_attrs*, *edge_attrs* are dictionaries containing
        key/value pairs to pass on as graphviz properties.
        """

        inheritance_graph_attrs = dict(fontsize=9, ratio='auto', size='""', rankdir="LR")
        inheritance_node_attrs = {"align": "center", 'shape': 'box',
                                  'fontsize': 9, 'height': 0.3,
                                  'fontname': 'Vera Sans, DejaVu Sans, Liberation Sans, '
                                  'Arial, Helvetica, sans', 'style': '"setlinewidth(0.5)"',
                                  'labelloc': 'c', 'fontcolor': 'grey45'}

        inheritance_edge_attrs = {'arrowsize': 0.5, 'style': '"setlinewidth(0.5)"', "color": "black"}
        
        g_attrs = self.default_graph_attrs.copy()
        n_attrs = self.default_node_attrs.copy()
        e_attrs = self.default_edge_attrs.copy()
        g_attrs.update(inheritance_graph_attrs)
        n_attrs.update(inheritance_node_attrs)
        e_attrs.update(inheritance_edge_attrs)

        res = []
        res.append('digraph %s {\n' % name)
        res.append(self._format_graph_attrs(g_attrs))

        for name, fullname, bases in self.class_info:
            # Write the node
            this_node_attrs = n_attrs.copy()

            if fullname in self.specials:
                this_node_attrs['fontcolor'] = 'black'
                this_node_attrs['color'] = 'blue'
                this_node_attrs['style'] = 'bold'

            this_node_attrs['URL'] = '"%s"'%fullname            
            res.append('  "%s" [%s];\n' %
                       (name, self._format_node_attrs(this_node_attrs)))

            # Write the edges
            for base_name in bases:

                this_edge_attrs = e_attrs.copy()
                if name in self.specials:
                    this_edge_attrs['color'] = 'red'
                    
                res.append('  "%s" -> "%s" [%s];\n' %
                           (base_name, name,
                            self._format_node_attrs(this_edge_attrs)))
        res.append('}\n')
        return ''.join(res)

    
class ParentBase(object):
    
    def __init__(self, name, kind):

        self.name = name
        self.kind = kind

        self.docs = u''
        self.filter = u''
        self.comments = u''

        self.is_redundant = False

        self.children = []

        self.id = NewId()
        

    def Add(self, klass):

        if u'lambda' in klass.name:
            return

        for child in self.children:
            if child.name == klass.name:
                return
            
        klass.parent = self
        self.children.append(klass)


    def Save(self):

        self.children = sorted(self.children, key=operator.attrgetter("order", "name"))

        if self.docs is None:
            self.docs = u''

        if self.comments is None or not self.comments.strip():
            self.comments = u''
            
        for child in self.children:
            child.Save()


    def GetImage(self):

        return self.kind


    def GetName(self):

        return self.name


    def GetShortName(self):

        return self.name.split(".")[-1]


    def GetObject(self):

        return self.obj_type
    

    def GetChildren(self):

        return self.children


    def GetChildrenCount(self, recursively=True):
        """
        Gets the number of children of this item.

        :param bool `recursively`: if ``True``, returns the total number of descendants,
         otherwise only one level of children is counted.
        """

        count = len(self.children)
        
        if not recursively:
            return count

        total = count

        for n in xrange(count):
            total += self.children[n].GetChildrenCount()
        
        return total

        
    def GetKindCount(self, minObj, maxObj=None):

        if maxObj is None:
            maxObj = minObj

        count = 0
        for child in self.children:
            if minObj <= child.kind <= maxObj:
                count += 1

        return count                


    def FormatOutput(self):

        kind = object_types.whatis(self.kind)

        modules = self.GetKindCount(object_types.PACKAGE, object_types.PYW_MODULE)
        classes = self.GetKindCount(object_types.KLASS)
        methods = self.GetKindCount(object_types.METHOD, object_types.BUILTIN_FUNCTION)
        properties = self.GetKindCount(object_types.PROPERTY)
        attributes = self.GetKindCount(object_types.BOOLTYPE, sys.maxint)
        
        return self.obj_type, self.GetShortName(), self.parent.GetName(), kind, self.filename, '%d'%modules, '%d'%classes, '%d'%methods, '%d'%properties, '%d'%attributes


    def MakeDiff(self, other, ignoreList):

        if self.kind != other.kind and KIND_DIFF not in ignoreList:
            return KIND_DIFF

        return NO_DIFF


class Library(ParentBase):

    def __init__(self, name):

        ParentBase.__init__(self, name, object_types.LIBRARY)

        self.parent = None
        self.filename = u''
        self.order = 0
        self.obj_type = u"Library"
        self.python_version = u''


    def FormatOutput(self):

        kind = object_types.whatis(self.kind)

        modules = self.GetKindCount(object_types.PACKAGE, object_types.PYW_MODULE)
        classes = self.GetKindCount(object_types.KLASS)
        methods = self.GetKindCount(object_types.METHOD, object_types.BUILTIN_FUNCTION)
        properties = self.GetKindCount(object_types.PROPERTY)
        attributes = self.GetKindCount(object_types.BOOLTYPE, sys.maxint)
        
        return self.obj_type, self.GetName(), 'None', kind, self.filename, '%d'%modules, '%d'%classes, '%d'%methods, '%d'%properties, '%d'%attributes


    def GetShortName(self):

        return self.name        
    

    def Walk(self, obj, filter=u''):

        # initialize
        result = []

        # must have at least root folder
        children = obj.GetChildren()

        if not children:
            return result

        # check each name
        for child in children:
            name = child.GetShortName().lower()
            if filter in name:
                result.append(child)
                    
            # recursively scan other folders, appending results
            result = result + self.Walk(child, filter)

        return result


    def Compare(self, me, you, ignoreList):

        # initialize
        result = []

        # must have at least root folder
        my_children = me.GetChildren()
        your_children = you.GetChildren()

        if not my_children and not your_children:
            return result

        dict_one, dict_two = {}, {}

        for child in my_children:
            dict_one[child.name] = child

        for child in your_children:
            dict_two[child.name] = child

        children_one = set(dict_one.keys())
        children_two = set(dict_two.keys())

        common = list(children_one & children_two)
        one_only = list(children_one - children_two)
        two_only = list(children_two - children_one)

        if ONE_ONLY not in ignoreList:
            for name in one_only:
                child = dict_one[name]
                result.append((child, child, ONE_ONLY))

        if TWO_ONLY not in ignoreList:
            for name in two_only:
                child = dict_two[name]
                result.append((child, child, TWO_ONLY))
                        
        for name in common:
            my_child = dict_one[name]
            your_child = dict_two[name]
            
            diffs = my_child.MakeDiff(your_child, ignoreList)

            if diffs != NO_DIFF:
                result.append((my_child, your_child, diffs))

            result += self.Compare(my_child, your_child, ignoreList)

        return result


    def GetPythonVersion(self):

        return self.python_version
    
                
class Module(ParentBase):

    def __init__(self, name, kind):

        ParentBase.__init__(self, name, kind)
        
        self.filename = u''
        
        if kind == object_types.PACKAGE:
            self.obj_type = u"Package"
            self.order = kind
            return

        self.order = object_types.PY_MODULE
                    
        for dummy, icon, description in _moduleToIcon:
            if icon == kind:
                self.obj_type = description
                break

        self.inheritance_diagram = None
        

    def MakeDiff(self, other, ignoreList):

        diff = ParentBase.MakeDiff(self, other, ignoreList)

        if diff != NO_DIFF:
            return diff
        
        if DOCS_DIFF not in ignoreList and self.docs != other.docs:
            return DOCS_DIFF
        
        return NO_DIFF


class Class(ParentBase):

    def __init__(self, name, obj):

        ParentBase.__init__(self, name, object_types.KLASS)

        try:
            subs = obj.__subclasses__()
        except (AttributeError, TypeError):
            subs = []

        sups = list(obj.__bases__)
            
        sortedSubClasses = []
        sortedSupClasses = []

        for item in sups:
            item = repr(item)
            
##            if "object" in item:
##                continue

            sup = item.replace("<class ", "").replace(">", "").replace("<type ", "")
            sup = sup.strip().replace('"', "").replace("'", "")
            if " at " in sup:
                sup = sup[0:sup.index(" at ")].strip()
                
            sortedSupClasses.append(sup)

        sortedSupClasses.sort()    
           
        for s in subs:
            s = repr(s)

            if "'" in s:
                start = s.index("'")
                end = s.rindex("'")
                cls = s[start+1:end]
            else:
                cls = s

            sortedSubClasses.append(cls)

        sortedSubClasses.sort()
        
        self.class_tree = make_class_tree(getclasstree(getmro(obj)))
        
        self.subClasses = sortedSubClasses
        self.superClasses = sortedSupClasses

        self.signature = ""
        self.number_lines = 'Unknown'
        self.inheritance_diagram = None
        
        self.order = 3
        self.obj_type = u"Class"


    def FormatOutput(self):

        kind = object_types.whatis(self.kind)
        kind = kind.replace("KLASS", "CLASS")

        methods = self.GetKindCount(object_types.METHOD, object_types.BUILTIN_FUNCTION)
        properties = self.GetKindCount(object_types.PROPERTY)
        attributes = self.GetKindCount(object_types.BOOLTYPE, sys.maxint)
        
        return self.obj_type, self.GetShortName(), self.parent.GetName(), kind, self.number_lines, '%d'%methods, '%d'%properties, '%d'%attributes
    

    def MakeDiff(self, other, ignoreList):

        diff = ParentBase.MakeDiff(self, other, ignoreList)

        if diff != NO_DIFF:
            return diff
        
        if self.signature != other.signature and SIGNATURE_DIFF not in ignoreList:
            return SIGNATURE_DIFF

        if (self.subClasses != other.subClasses or self.superClasses != other.superClasses) and CLASSTREE_DIFF not in ignoreList:
            return CLASSTREE_DIFF

        if LOC_DIFF not in ignoreList and self.number_lines != other.number_lines:
            return LOC_DIFF

        if DOCS_DIFF not in ignoreList and self.docs != other.docs:
            return DOCS_DIFF
        
        return NO_DIFF
    

class ChildrenBase(object):
    
    def __init__(self, name, kind):

        self.name = name
        self.kind = kind

        self.order = 4        

        self.docs = u''
        self.comments = u''

        self.is_redundant = False

        self.id = NewId()


    def GetImage(self):

        return self.kind


    def GetName(self):

        return self.name


    def GetShortName(self):

        return self.name.split(".")[-1]


    def GetChildren(self):

        return []
    

    def GetChildrenCount(self, recursively=True):

        return 0        


    def GetObject(self):

        return self.obj_type


    def Save(self):
        
        if self.docs is None:
            self.docs = u''

        if self.comments is None or not self.comments.strip():
            self.comments = u''


    def MakeDiff(self, other, ignoreList):

        if self.kind != other.kind and KIND_DIFF not in ignoreList:
            return KIND_DIFF

        return NO_DIFF


class Method(ChildrenBase):

    def __init__(self, name, kind):

        ChildrenBase.__init__(self, name, kind)
        
        self.order = 5
        
        self.arguments = []
        self.signature = u''

        self.firstlineno = self.number_lines = 'Unknown'
        self.obj_type = u"Method/Function"
        

    def FormatOutput(self):

        kind = object_types.whatis(self.kind)        
        return self.obj_type, self.GetShortName(), self.parent.GetName(), kind, self.firstlineno, self.number_lines


    def Save(self):

        ChildrenBase.Save(self)        

        newargs = []
        if self.arguments and any(self.arguments[0]):
            for name, repr_val, eval_val in self.arguments:
                repr_val = (repr_val is not None and [repr_val] or [""])[0]
                eval_val = (eval_val is not None and [eval_val] or [""])[0]
                newargs.append((name, repr_val, eval_val))

        self.arguments = newargs


    def MakeDiff(self, other, ignoreList):

        diff = ChildrenBase.MakeDiff(self, other, ignoreList)

        if diff != NO_DIFF:
            return diff

        if SIGNATURE_DIFF not in ignoreList and self.signature != other.signature:
            return SIGNATURE_DIFF

        if LOC_DIFF not in ignoreList and self.number_lines != other.number_lines:
            return LOC_DIFF

        if FIRSTLINE_DIFF not in ignoreList and self.firstlineno != other.firstlineno:
            return FIRSTLINE_DIFF

        if DOCS_DIFF not in ignoreList and self.docs != other.docs:
            return DOCS_DIFF
        
        return NO_DIFF
    

class Property(ChildrenBase):

    def __init__(self, name, item):

        ChildrenBase.__init__(self, name, object_types.PROPERTY)
        
        self.getter = self.setter = self.deleter = ""

        try:
            if item.fget:
                self.getter = item.fget.__name__
            if item.fset:
                self.setter = item.fset.__name__
            if item.fdel:
                self.deleter = item.fdel.__name__
        except AttributeError:
            # Thank you for screwing it up, Cython...
            if item.fget:
                self.getter = item.fget.__class__.__name__
            if item.fset:
                self.setter = item.fset.__class__.__name__
            if item.fdel:
                self.deleter = item.fdel.__class__.__name__
            
        self.docs = getdoc(item)
        self.comments = getcomments(item)
        
        self.obj_type = u"Property"
        self.order = 6


    def FormatOutput(self):

        kind = object_types.whatis(self.kind)
        return self.obj_type, self.GetShortName(), self.parent.GetName(), kind, self.getter, self.setter, self.deleter


    def MakeDiff(self, other, ignoreList):

        diff = ChildrenBase.MakeDiff(self, other, ignoreList)

        if diff != NO_DIFF:
            return diff

        if self.kind == other.kind:
            if GETSET_DIFF not in ignoreList and (self.setter != other.setter or self.getter != other.getter or self.deleter != other.deleter):
                return GETSET_DIFF

        if DOCS_DIFF not in ignoreList and self.docs != other.docs:
            return DOCS_DIFF

        return NO_DIFF
    

class Attribute(ChildrenBase):

    def __init__(self, name, specs, value):

        specs = unicode(specs)        
        start, end = specs.find("'"), specs.rfind("'")
        specs = specs[start+1:end]

        strValue = repr(value)
        uspecs = specs.upper()

        try:
            kind = getattr(object_types, uspecs)
        except AttributeError:
            try:
                uspecs = uspecs + u"TYPE"
                kind = getattr(object_types, uspecs)
            except AttributeError:                
                kind = object_types.UNKNOWNTYPE
        
        try:
            reprValue = repr(value.__class__)
        except (NameError, AttributeError):
            reprValue = ""
            
        if u"class" in strValue or u"class" in reprValue:
            kind = object_types.INSTANCETYPE

        ChildrenBase.__init__(self, name, kind)
        
        self.value = strValue
        self.specs = specs

        try:
            self.docs = getdoc(value)
        except (NameError, AttributeError):
            self.docs = u''

        self.obj_type = u"Attribute"
        self.order = 7
        

    def FormatOutput(self):

        kind = object_types.whatis(self.kind)
        return self.obj_type, self.GetShortName(), self.parent.GetName(), kind, self.value


    def MakeDiff(self, other, ignoreList):

        diff = ChildrenBase.MakeDiff(self, other, ignoreList)

        if diff != NO_DIFF:
            return diff

        if VALUE_DIFF not in ignoreList and self.value != other.value:
            return VALUE_DIFF

        if DOCS_DIFF not in ignoreList and self.docs != other.docs:
            return DOCS_DIFF

        return NO_DIFF


